library(rlang)8 Conditions
Introduction
情况系统(Condition System)包含两部分,一方面是函数内部根据不同情况生成不同等级的信息,另一方面是函数使用者根据返回的信息进行不同的处理。
R 提供了一个基于 Common Lisp 思想的非常强大的情况系统。本章介绍 R 情况系统的主要思想,以及一些实用工具,这些工具将使你的代码更加健壮。
Outline
- 8.2 节介绍了情况系统的基本工具,并讨论了何时适合使用每种工具。
- 8.3 节介绍最简单的情况处理工具:像
try()和suppressMessages()这样的函数,它们会吞噬情况信息并阻止其达到顶层。 - 8.4 节介绍了情况对象,以及两个基本的情况处理工具:用于错误情况的
tryCatch()和用于其他一切的withCallingHandlers()。 - 8.5 节展示了如何扩展内置情况对象,以存储情况处理程序可用于做出更明智决策的有用数据。
- 8.6 节以一系列基于前面章节中提到的低级工具的实际应用程序作为本章的结尾。
Prerequisites
本章使用rlang包中的状态信号与处理函数。
Signalling conditions
R 提供了三种情况信号:errors,warnings,messages。
- error:最严重,表示函数无法继续执行,必须终止。
- warning:次之,表示函数内部某些是错的,但是不影响函数运行。
- message:仅用于显示函数内某些动作的状态。
情况系统的信息通常是瞩目的,例如加粗,红色等。
stop("This is what an error looks like")
#> Error: This is what an error looks like
warning("This is what a warning looks like")
#> Warning: This is what a warning looks like
message("This is what a message looks like")
#> This is what a message looks likeErrors
base R 通过stop()函数抛出错误信息。
f <- function() g()
g <- function() h()
h <- function() stop("This is an error!")
f()
#> Error in h(): This is an error!stop()函数有参数.call,控制是否进行调用栈朔源(traceback()也可以)。
h <- function() stop("This is an error!", call. = FALSE)
f()
#> Error: This is an error!rlang 包中的abort()与stop()等价,但其功能更加全面,后面我们会继续介绍它。
h <- function() abort("This is an error!")
f()
#> Error in `h()`:
#> ! This is an error!错误信息最好可以指出哪里处了问题,引导用户改进。但是编写好的错误信息很困难,因为错误通常发生在用户对函数有一个有缺陷的心理模型时。作为开发人员,很难想象用户会如何错误地思考你的函数,因此很难编写一条能够引导用户走向正确方向的信息。
Warnings
警告信息比错误信息弱,它表示程序某些地方出错,但不影响程序正常运行。函数内可以有多条警告信息。
fw <- function() {
cat("1\n")
warning("W1")
cat("2\n")
warning("W2")
cat("3\n")
warning("W3")
}与错误信息不同,警告信息默认在程序运行中缓存,结束后显示。
fw()
#> 1
#> Warning in fw(): W1
#> 2
#> Warning in fw(): W2
#> 3
#> Warning in fw(): W3options()可以设置警告信息的行为。
options(warn = 0):默认设置。options(warn = 1):警告信息会立即显示。options(warn = 0):警告信息视作错误信息。
warning()函数同样有call.参数,建议设置为FALSE。rlang 中也有类似函数rlang::warn()。
base R 中的有些警告信息,作者认为改写为报错信息会给更好。例如:
formals(1)
#> Warning in formals(fun): argument is not a function
#> NULL
file.remove("this-file-doesn't-exist")
#> Warning in file.remove("this-file-doesn't-exist"): cannot remove file
#> 'this-file-doesn't-exist', reason 'No such file or directory'
#> [1] FALSE
lag(1:3, k = 1.5)
#> Warning in lag.default(1:3, k = 1.5): 'k' is not an integer
#> [1] 1 2 3
#> attr(,"tsp")
#> [1] -1 1 1
as.numeric(c("18", "30", "50+", "345,678"))
#> Warning: NAs introduced by coercion
#> [1] 18 30 NA NA有两种情况,使用警告信息会更好:
当你升级了某个函数,但是不推荐使用它时,可以打印一个版本警告信息。
当你确定可以通过警告信息提醒使用者正确使用函数时。
Messages
提示信息由message()函数生成,函数没有call.参数,生成的提示信息会实时打印在控制台。恰到好处的提示信息可以告诉使用者,你的程序运行到了哪里,此刻的运行状态是什么。
fm <- function() {
cat("1\n")
message("M1")
cat("2\n")
message("M2")
cat("3\n")
message("M3")
}
fm()
#> 1
#> M1
#> 2
#> M2
#> 3
#> M3下面是一些使用提示信息的情况:
当函数的默认参数值需要一些计算时,你需要告诉使用者计算的情况。例如ggplot中的
binwidth参数,如果用户没有指定参数,ggplot会根据数据集自动计算一个合适的参数值。当函数调用了其他必要且耗时的函数时,你需要告诉使用者,你的程序正在做什么。
当函数运行耗时特别长时,你需要提供一个进度条。
为R包添加加载后的提示信息(使用
packageStartupMessage())。
每个函数都应当有一个quiet = TRUE参数,用来禁用提示信息。
cat()函数与messages()函数类似,但是cat()函数面向使用者,而messages()函数面向开发者。
cat("Hi!\n")
#> Hi!
message("Hi!")
#> Hi!Ignoring conditions
base R 中忽略三种信息的方法:
try():忽略所有错误信息。suppressWarnings():忽略所有警告信息。suppressMessages():忽略所有提示信息。
这三种方法的共同缺点是,无法忽略单一某条信息,而保证其他信息通过,它们的作用是全局的。
try()
通常函数报错会停止运行,try() 函数可以忽略错误信息,让函数继续执行。
f1 <- function(x) {
log(x)
10
}
f1("x")
#> Error in log(x): non-numeric argument to mathematical function
f2 <- function(x) {
try(log(x))
10
}
f2("a")
#> Error in log(x) : non-numeric argument to mathematical function
#> [1] 10为了实现根据运行情况(成功或失败)返回不同的值时,不建议将try()的结果直接赋值给变量,而是事先定义变量,然后在try()内部进行赋值。除了try()函数,也可以使用更高级的tryCatch()函数。
# 不推荐
default <- try(read.csv("possibly-bad-input.csv"), silent = TRUE)
#> Warning in file(file, "rt"): cannot open file 'possibly-bad-input.csv': No
#> such file or directory
# 推荐
default <- NULL
try(default <- read.csv("possibly-bad-input.csv"), silent = TRUE)
#> Warning in file(file, "rt"): cannot open file 'possibly-bad-input.csv': No
#> such file or directorysuppress*
suppressWarnings({
warning("Uhoh!")
warning("Another warning")
1
})
#> [1] 1
suppressMessages({
message("Hello there")
2
})
#> [1] 2
suppressWarnings({
message("You can still see me")
3
})
#> You can still see me
#> [1] 3Handling conditions
每种情况都有默认行为:错误信息终止程序运行,警告信息在程序运行结束后打印,提示信息即时打印。情况处理系统允许我们暂时压制或补充这些默认行为。
base R 提供了两个函数 tryCatch() 和 withCallingHandlers() 来处理情况。前者在情况触发时进入到退出函数(exiting handlers),适合处理错误情况;后者在情况触发时会接着运行(calling handlers),适合处理警告和提示情况。
tryCatch(
error = function(cnd) {
# code to run when error is thrown
},
code_to_run_while_handlers_are_active
)
withCallingHandlers(
warning = function(cnd) {
# code to run when warning is signalled
},
message = function(cnd) {
# code to run when message is signalled
},
code_to_run_while_handlers_are_active
)Condition objects
每种情况触发时,都会创建一个不被我们看到的condition对象,使用rlang::catch_cnd()函数可以查看此对象。
# cnd <- stop("An error") # 也可以,但是会显示报错信息
cnd <- catch_cnd(stop("An error"))
str(cnd)
#> List of 2
#> $ message: chr "An error"
#> $ call : language force(expr)
#> - attr(*, "class")= chr [1:3] "simpleError" "error" "condition"
conditionMessage(cnd)
#> [1] "An error"
conditionCall(cnd)
#> force(expr)condition对象包含两个元素:
message:长度为1的字符串,用来展示信息。可以使用conditionMessage()函数查看。call:触发情况的函数调用,如果参数call. = FALSE则为NULL。可以使用conditionCall()函数查看。
自定义的condition对象也可以包含其他元素。
该对象同时具有class属性,表示对象属于S3类。
Exiting handlers
tryCatch()函数通常用在错误情况处理中,能够覆盖默认的错误行为。例如下面的函数在错误时返回NA。
f3 <- function(x) {
tryCatch(
error = function(cnd) NA,
log(x)
)
}
f3("x")
#> [1] NA如果情况没有被触发或不符合定义的condition对象,则会正常运行。
tryCatch(
error = function(cnd) 10,
1 + 1
)
#> [1] 2
tryCatch(
error = function(cnd) 10,
{
message("Hi!")
1 + 1
}
)
#> Hi!
#> [1] 2tryCatch()定义的handler称作 exiting handler,因为在情况触发后,程序不会再运行触发情况的代码。
tryCatch(
message = function(cnd) "There",
{
message("Here")
stop("This code is never run!")
}
)
#> [1] "There"注意:定义的handler是一个函数,它的运行环境与外面代码的运行环境不用。
handler函数只接受一个参数——condition对象,可以提取对象中的信息,这对后续介绍的自定义condition对象十分有用。
tryCatch(
error = function(cnd) {
paste0("--", conditionMessage(cnd), "--")
},
stop("This is an error")
)
#> [1] "--This is an error--"finally
tryCatch()函数还有一个finally参数,接受一个代码块。其功能类似于on.exit(),无论情况是否触发,都会运行这段代码,通常用来清理缓存,删除临时文件或关闭链接等。
path <- tempfile()
tryCatch(
{
writeLines("Hi!", path)
# ...
},
finally = {
# always run
unlink(path)
}
)Calling handlers
withCallingHandlers()函数通常用来处理警告或提示情况。与tryCatch()不同,代码触发情况后,会执行handler函数,待handler函数运行结束后接着运行。这好像中间插入了一段运行代码。
下面是tryCatch()和withCallingHandlers()的比较:
tryCatch(
message = function(cnd) cat("Caught a message!\n"),
{
message("Someone there?")
message("Why, yes!")
}
)
#> Caught a message!
withCallingHandlers(
message = function(cnd) cat("Caught a message!\n"),
{
message("Someone there?")
message("Why, yes!")
}
)
#> Caught a message!
#> Someone there?
#> Caught a message!
#> Why, yes!handler函数按顺序执行,不必担心内部情况被捕捉,造成死循环。
withCallingHandlers(
message = function(cnd) message("Second message"),
message("First message")
)
#> Second message
#> First message但是要注意:如果有多个handler函数,某些handler函数的情况可能会被其他handler函数捕获,要考虑handler函数的顺序。
withCallingHandlers( # (1)
message = function(cnd) message("b"),
withCallingHandlers( # (2)
message = function(cnd) message("a"),
message("c")
)
)
#> b
#> a
#> b
#> cmuffle
withCallingHandlers()中的handler函数也会返回值,但是与tryCatch()不同,它的返回值没有被使用,calling handler 函数只发挥了它的副作用。其中一个重要副作用就是屏蔽信息。
当情况处理函数发生嵌套时,会自动触发父级handler函数。
# Bubbles all the way up to default handler which generates the message
withCallingHandlers(
message = function(cnd) cat("Level 2\n"),
withCallingHandlers(
message = function(cnd) cat("Level 1\n"),
message("Hello")
)
)
#> Level 1
#> Level 2
#> Hello
# Bubbles up to tryCatch
tryCatch(
message = function(cnd) cat("Level 2\n"),
withCallingHandlers(
message = function(cnd) cat("Level 1\n"),
message("Hello")
)
)
#> Level 1
#> Level 2可以使用rlang::cnd_muffle()来屏蔽信息。
# Muffles the default handler which prints the messages
withCallingHandlers(
message = function(cnd) {
cat("Level 2\n")
cnd_muffle(cnd)
},
withCallingHandlers(
message = function(cnd) cat("Level 1\n"),
message("Hello")
)
)
#> Level 1
#> Level 2
# Muffles level 2 handler and the default handler
withCallingHandlers(
message = function(cnd) cat("Level 2\n"),
withCallingHandlers(
message = function(cnd) {
cat("Level 1\n")
cnd_muffle(cnd)
},
message("Hello")
)
)
#> Level 1Call stacks
exiting handler 函数与 calling handler 函数的调用栈不同。
f <- function() g()
g <- function() h()
h <- function() message("!")calling handler 是在函数f()的调用栈中被调用
withCallingHandlers(
f(),
message = function(cnd) {
lobstr::cst()
cnd_muffle(cnd)
}
)
#> ▆
#> 1. ├─base::withCallingHandlers(...)
#> 2. ├─global f()
#> 3. │ └─global g()
#> 4. │ └─global h()
#> 5. │ └─base::message("!")
#> 6. │ ├─base::withRestarts(...)
#> 7. │ │ └─base (local) withOneRestart(expr, restarts[[1L]])
#> 8. │ │ └─base (local) doWithOneRestart(return(expr), restart)
#> 9. │ └─base::signalCondition(cond)
#> 10. └─global `<fn>`(`<smplMssg>`)
#> 11. └─lobstr::cst()exiting handler 是在函数tryCatch()的调用栈中被调用
tryCatch(
f(),
message = function(cnd) lobstr::cst()
)
#> ▆
#> 1. └─base::tryCatch(f(), message = function(cnd) lobstr::cst())
#> 2. └─base (local) tryCatchList(expr, classes, parentenv, handlers)
#> 3. └─base (local) tryCatchOne(expr, names, parentenv, handlers[[1L]])
#> 4. └─value[[3L]](cond)
#> 5. └─lobstr::cst()catch_cnd(stop("An error"))
#> <simpleError in force(expr): An error>
catch_cnd(abort("An error"))
#> <error/rlang_error>
#> Error:
#> ! An error
#> ---
#> Backtrace:
#> ▆Custom conditions
base R 内置的condition对象包含的信息有限——message和call。rlang包提供了额外的函数——abort(),warn(),inform(),singal(),帮助自定义condition对象。它们的使用方法和base R 中的一样,例如:rlang::abort()通过参数class添加额外的类和附加信息。
abort(
class = "error_not_found",
message = "Path `blah.csv` not found",
path = "blah.csv"
)
#> Error:
#> ! Path `blah.csv` not foundMotivation
下面以base::log()函数为例,阐述自定义condition对象的优势。
当参数不符合标准时,log()会返回一个错误。
log(letters)
#> Error in log(letters): non-numeric argument to mathematical function
log(1:10, base = letters)
#> Error in log(1:10, base = letters): non-numeric argument to mathematical function上面的报错信息不是很友好,因为它没有具体指出那个参数错误,原因是什么。我们可以进行下面的修改:
my_log <- function(x, base = exp(1)) {
if (!is.numeric(x)) {
abort(paste0(
"`x` must be a numeric vector; not ", typeof(x), "."
))
}
if (!is.numeric(base)) {
abort(paste0(
"`base` must be a numeric vector; not ", typeof(base), "."
))
}
base::log(x, base = base)
}my_log(letters)
#> Error in `my_log()`:
#> ! `x` must be a numeric vector; not character.
my_log(1:10, base = letters)
#> Error in `my_log()`:
#> ! `base` must be a numeric vector; not character.现在的报错信息就显得用户友好了,但是对于开发者来说不够友好,所有关键信息都被储存在了报错信息中,我们无法函数式地编写这类报错。
Signalling
为了实现上述功能,我们先自定义一个abort()函数:函数通过glue::glue()将附加信息拼接到错误信息中,然后传递到abort()函数中。注意我们定义了一个新condition类型——error_bad_argument。
abort_bad_argument <- function(arg, must, not = NULL) {
msg <- glue::glue("`{arg}` must {must}")
if (!is.null(not)) {
not <- typeof(not)
msg <- glue::glue("{msg}; not {not}.")
}
abort("error_bad_argument",
message = msg,
arg = arg,
must = must,
not = not
)
}不基于rlang包,也可以实现上面的功能:
stop_custom <- function(.subclass, message, call = NULL, ...) {
err <- structure(
list(
message = message,
call = call,
...
),
class = c(.subclass, "error", "condition")
)
stop(err)
}
err <- catch_cnd(
stop_custom("error_new", "This is a custom error", x = 10)
)
class(err)
#> [1] "error_new" "error" "condition"
err$x
#> [1] 10现在我们可以重新改写my_log():
my_log <- function(x, base = exp(1)) {
if (!is.numeric(x)) {
abort_bad_argument("x", must = "be numeric", not = x)
}
if (!is.numeric(base)) {
abort_bad_argument("base", must = "be numeric", not = base)
}
base::log(x, base = base)
}my_log(letters)
#> Error in `abort_bad_argument()`:
#> ! `x` must be numeric; not character.
my_log(1:10, base = letters)
#> Error in `abort_bad_argument()`:
#> ! `base` must be numeric; not character.Handling
自定义的condition类十分利于编程。
我们可以使用testthat中的函数来检测这个类包含的内容是否符合预期:
library(testthat)
#>
#> Attaching package: 'testthat'
#> The following objects are masked from 'package:rlang':
#>
#> is_false, is_null, is_true
err <- catch_cnd(my_log("a"))
expect_s3_class(err, "error_bad_argument")
expect_equal(err$arg, "x")
expect_equal(err$not, "character")自定义的类也可以用在handler函数中:
tryCatch(
error_bad_argument = function(cnd) "bad_argument",
error = function(cnd) "other error",
my_log("a")
)
#> [1] "bad_argument"需要注意的是,因为自定义的类属于子类,所以无法完美的进行类判断。handler函数的顺序会直接影响类的判断结果。
tryCatch(
error = function(cnd) "other error",
error_bad_argument = function(cnd) "bad_argument",
my_log("a")
)
#> [1] "other error"Applications
本节介绍一些使用tryCatch()和withCallingHandlers()的常见模式。
Failure value
tryCatch()的errorhandler函数返回默认值。
fail_with <- function(expr, value = NULL) {
tryCatch(
error = function(cnd) value,
expr
)
}
fail_with(log(10), NA_real_)
#> [1] 2.302585
fail_with(log("x"), NA_real_)
#> [1] NA创建base::try()的类似函数try2()。
try2 <- function(expr, silent = FALSE) {
tryCatch(
error = function(cnd) {
msg <- conditionMessage(cnd)
if (!silent) {
message("Error: ", msg)
}
structure(msg, class = "try-error")
},
expr
)
}
try2(1)
#> [1] 1
try2(stop("Hi"))
#> Error: Hi
#> [1] "Hi"
#> attr(,"class")
#> [1] "try-error"
try2(stop("Hi"), silent = TRUE)
#> [1] "Hi"
#> attr(,"class")
#> [1] "try-error"Success and failure values
将上面的模式再进一步改写为:成功返回一个值,失败返回另一个值。
foo <- function(expr) {
tryCatch(
error = function(cnd) error_val,
{
expr
success_val
}
)
}也可以改写为检测是否成功:
does_error <- function(expr) {
tryCatch(
error = function(cnd) TRUE,
{
expr
FALSE
}
)
}还可以捕获condtion对象,类似于rlang::catch_cnd():
catch_cnd <- function(expr) {
tryCatch(
condition = function(cnd) cnd,
{
expr
NULL
}
)
}利用这一模式,我们可以创建一个try()变体,同时返回错误对象和结果:
safety <- function(expr) {
tryCatch(
error = function(cnd) {
list(result = NULL, error = cnd)
},
list(result = expr, error = NULL)
)
}
str(safety(1 + 10))
#> List of 2
#> $ result: num 11
#> $ error : NULL
str(safety(stop("Error!")))
#> List of 2
#> $ result: NULL
#> $ error :List of 2
#> ..$ message: chr "Error!"
#> ..$ call : language doTryCatch(return(expr), name, parentenv, handler)
#> ..- attr(*, "class")= chr [1:3] "simpleError" "error" "condition"上面的safety()函数类似于purrr::safely(),我们将在11章中讨论它。
Resignal
前面讲到可以通过options(warn = 2)将警告转为错误。但这种做法是全局修改,我们可以构造下面的函数,单独将警告转为错误。
warning2error <- function(expr) {
withCallingHandlers(
warning = function(cnd) abort(conditionMessage(cnd)),
expr
)
}warning2error({
x <- 2^4
warn("Hello")
})
#> Error:
#> ! Hello这个函数也可以用来查找那些经常出现但有不知道来源的信息,更多信息见22章。
Record
修改handler函数,实现记录每条信息。
catch_cnds <- function(expr) {
conds <- list()
add_cond <- function(cnd) {
conds <<- append(conds, list(cnd))
cnd_muffle(cnd)
}
withCallingHandlers(
message = add_cond,
warning = add_cond,
expr
)
conds
}
catch_cnds({
inform("a")
warn("b")
inform("c")
})
#> [[1]]
#> <message/rlang_message>
#> Message:
#> a
#>
#> [[2]]
#> <warning/rlang_warning>
#> Warning:
#> b
#>
#> [[3]]
#> <message/rlang_message>
#> Message:
#> c如果想捕获错误信息,需要将withCallingHandlers()置于tryCatch()中。
catch_cnds <- function(expr) {
conds <- list()
add_cond <- function(cnd) {
conds <<- append(conds, list(cnd))
cnd_muffle(cnd)
}
tryCatch(
error = function(cnd) {
conds <<- append(conds, list(cnd))
},
withCallingHandlers(
message = add_cond,
warning = add_cond,
expr
)
)
conds
}
catch_cnds({
inform("a")
warn("b")
abort("C")
})
#> [[1]]
#> <message/rlang_message>
#> Message:
#> a
#>
#> [[2]]
#> <warning/rlang_warning>
#> Warning:
#> b
#>
#> [[3]]
#> <error/rlang_error>
#> Error:
#> ! C
#> ---
#> Backtrace:
#> ▆这种模式同时也是evaluate包的主要思想,该包用于knitr中。
No default behaviour
使用rlang::signal()函数可以创建不基于message,warning,error的condition类。
log <- function(message, level = c("info", "error", "fatal")) {
level <- match.arg(level)
signal(message, "log", level = level)
}因为没有默认的handler函数,log()函数不会打印任何信息。
log("This code was run")搭配withCallingHandlers()函数,可以定义一个log的handler函数。
record_log <- function(expr, path = stdout()) {
withCallingHandlers(
log = function(cnd) {
cat(
"[", cnd$level, "] ", cnd$message, "\n",
sep = "",
file = path, append = TRUE
)
},
expr
)
}
record_log(log("Hello"))
#> [info] Hello也可以创建一个不显示某个日志级别信息的handler函数。
ignore_log_levels <- function(expr, levels) {
withCallingHandlers(
log = function(cnd) {
if (cnd$level %in% levels) {
cnd_muffle(cnd)
}
},
expr
)
}
record_log(ignore_log_levels(log("Hello"), "info"))如果你手动创建了一个condition对象,并且通过signalCondition()触发,rlang::cnd_muffle()将不会工作。需要搭配withRestarts()。
withRestarts(signalCondition(cond), muffle = function() NULL)